package mn.more.mock.io.xml;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Array;
import java.lang.reflect.Method;
import java.net.URLDecoder;
import java.util.HashMap;
import java.util.Map;
import mn.more.foundation.lang.StringUtil;
import mn.more.foundation.xml.JdomUtil;
import mn.more.mock.io.MockDataWriter;
import mn.more.mock.io.MockModel;
import org.jdom.CDATA;
import org.jdom.DocType;
import org.jdom.Document;
import org.jdom.Element;

/**
 * @author <a href="mailto:mike.liu@aptechmongolia.edu.mn">Mike Liu</a>
 * @version $Id: XMLWriter.java 5 2008-09-01 12:08:42Z mikeliucc $
 */
public class XMLWriter implements MockDataWriter {

	// todo: find another XML-friendly character
	//public static final String DELIM = "@#EOC#@";
	public static final String DELIM = String.valueOf((char) 254);
	public static final String DTD_LOCATION = "model.dtd";
	public static final String ROOT_NAME = "models";
	public static final Map<String, String> TYPE_NAME_MAP = initTypeNameMap();

	private String filename;

	private static Map<String, String> initTypeNameMap() {
		Map<String, String> map = new HashMap<String, String>();
		map.put(boolean.class.getSimpleName(), "boolean");
		map.put(byte.class.getSimpleName(), "byte");
		map.put(short.class.getSimpleName(), "short");
		map.put(char.class.getSimpleName(), "char");
		map.put(int.class.getSimpleName(), "int");
		map.put(Integer.class.getSimpleName(), "int");
		map.put(long.class.getSimpleName(), "long");
		map.put(float.class.getSimpleName(), "float");
		map.put(double.class.getSimpleName(), "double");
		map.put(String.class.getSimpleName(), "String");
		map.put(byte[].class.getSimpleName(), "byteArray");
		map.put(short[].class.getSimpleName(), "shortArray");
		map.put(char[].class.getSimpleName(), "charArray");
		map.put(int[].class.getSimpleName(), "intArray");
		map.put(Integer[].class.getSimpleName(), "intArray");
		map.put(long[].class.getSimpleName(), "longArray");
		map.put(float[].class.getSimpleName(), "floatArray");
		map.put(double[].class.getSimpleName(), "doubleArray");
		map.put(String[].class.getSimpleName(), "StringArray");
		return map;
	}

	public String getFilename() { return filename; }

	public void setFilename(String filename, boolean classpath) {
		if (classpath) {
			try {
				// need to wrap urldecoder due to spaces in directory names
				this.filename = URLDecoder.decode(getClass().getResource("/").getFile(), "UTF-8") + filename;
			} catch (UnsupportedEncodingException e) {
				throw new RuntimeException("System does not support UTF-8!", e);
			}
		} else {
			this.filename = filename;
		}
	}

	public void setFile(File file) { this.filename = file.getAbsolutePath(); }

	/**
	 * this method will throw {@link ClassCastException} if the specified type is not one of the predefined ones as
	 * specified in the DTD (i.e. boolean, byte, char, short, int, long, float, double, String).  Furthermore, due to its
	 * recursive nature, the referenced models will be written before the referencing models.
	 */
	public void write(MockModel model) throws IOException {
		Element root = new Element(ROOT_NAME);

		// first pass to get all child property that are of type MockData
		addProperties(model, root);

		// hardcode the DTD file here.  XMLReader will resolve it using its own EntityResolver
		DocType docType = new DocType(ROOT_NAME, DTD_LOCATION);
		Document doc = new Document(root, docType);

		FileWriter writer = null;
		try {
			writer = new FileWriter(filename);
			// todo: deal with whitespaces in content (CDATA)
			// (Mike Liu, 2008.05.23): no need, we don't need to anymore... caller of this class will
			// use non-breaking whitespace instead (&nbsp;)
			writer.write(JdomUtil.toPrettyString(doc));
		} finally {
			try {
				if (writer != null) { writer.close(); }
			} catch (IOException e) {
			}
		}
	}

	private static void addProperties(MockModel model, Element root) {
		Element elemModel = new Element("model");
		elemModel.setAttribute("id", model.getId());
		for (String propertyName : model.getPropertyNames()) {
			Class type = model.getPropertyType(propertyName);
			Element elemProp = new Element("property");
			elemProp.setAttribute("name", propertyName);
			if (type == MockModel.class) {
				MockModel refmodel = model.getRefModel(propertyName);
				addProperties(refmodel, root);
				elemProp.setAttribute("refid", refmodel.getId());
			} else {
				String typeName = convertTypeName(type);
				Object value = model.getValue(propertyName);
				elemProp.setAttribute("type", typeName);
				if (typeName.indexOf("Array") > -1) {
					typeName = typeName.substring(0, typeName.indexOf("Array"));
					if (!value.getClass().isArray()) {
						throw new ClassCastException("expect array value for property '" + typeName + "'");
					}
					String strValue = "";
					int length = Array.getLength(value);
					for (int i = 0; i < length; i++) {
						try {
							Method m = Array.class.getMethod(
								"get" + (typeName.equalsIgnoreCase("String") ? "" : StringUtil.toTitleCase(typeName)),
								Object.class, int.class);
							strValue += m.invoke(null, value, i);
							if (i < (length - 1)) { strValue += DELIM; }
						} catch (Exception e) {
							throw new ClassCastException("array not of type " + type + " as expected.");
						}
					}
					elemProp.setContent(new CDATA(strValue));
				} else {
					elemProp.setContent(new CDATA(value == null ? "" : value.toString()));
				}
			}
			elemModel.addContent(elemProp);
		}
		root.addContent(elemModel);
	}

	private static String convertTypeName(Class type) {
		if (TYPE_NAME_MAP.containsKey(type.getSimpleName())) { return TYPE_NAME_MAP.get(type.getSimpleName()); }
		throw new ClassCastException("Property data type '" + type.getSimpleName() + "' not supported!");
	}
}
